/*
  author Sylvain Bertrand <sylvain.bertrand@gmail.com>
  Protected by linux GNU GPLv2
  Copyright 2012-2014
*/

#include <linux/module.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <asm/unaligned.h>
#include <linux/mutex.h>
#include <linux/delay.h>

#include <alga/timing.h>
#include <alga/dp.h>
#include <alga/amd/atombios/atb.h>
#include <alga/amd/atombios/dce.h>
#include <alga/amd/atombios/vram_info.h>

#include "tables/atb.h"
#include "tables/cmd.h"
#include "tables/data.h"
#include "tables/vram_info.h"

#include "atb.h"
#include "regs.h"

#if ATB_MC_REG_SETS_N_MAX != REG_SETS_N_MAX
#  error ATB_MC_REG_SETS_N_MAX != REG_SETS_N_MAX
#endif

#if ATB_MC_REGS_N_MAX != REGS_N_MAX
#  error ATB_MC_REGS_N_MAX != REGS_N_MAX
#endif

static inline u32 get(u32 mask, u32 v)
{
	u8 shift;

	shift = ffs(mask) - 1;
	return (v & mask) >> shift;
}

static long regs_addr_cpy(struct atombios *atb, struct reg_tbl_partial *reg_tbl,
		struct atb_mc_reg_tbl *atb_mc_reg_tbl, u8 *regs_val_location)
{
	u8 regs_n;
	u16 reg_idxs_sz;
	u8 reg;
	struct reg_idx *reg_idx;

	reg_idxs_sz = get_unaligned_le16(&reg_tbl->idxs_sz);
	/* remove the terminating REG_IDX_MISC_TERMINATOR element */
	regs_n = reg_idxs_sz / sizeof(*reg_idx) - 1;
	if (regs_n > REGS_N_MAX) {
		dev_err(atb->adev.dev, "atombios:vram_info too many memory controller registers\n");
		return -ATB_ERR;
	}

	/* point to the first struct reg_idx of the array in the tbl */
	reg_idx = (struct reg_idx*)((u8*)reg_tbl + sizeof(*reg_tbl));
	reg = 0;
	while (reg < regs_n) {
		if (reg_idx->misc & REG_IDX_MISC_TERMINATOR)
			break;

		atb_mc_reg_tbl->addrs[reg] = (u32)(
					get_unaligned_le16(&reg_idx->idx)
									<< 2);
		regs_val_location[reg] = reg_idx->misc
						& REG_IDX_MISC_REG_VAL_LOCATION;

		++reg;
		++reg_idx;
	}
	atb_mc_reg_tbl->addrs_n = reg;
	return 0;
}

static void regs_vals_cpy(struct atb_mc_reg_tbl *atb_mc_reg_tbl,
		u8 *regs_val_location, u8 set_idx, struct reg_set_partial *set)
{
	__le32 *cur_reg_val;
	u8 reg;

	/* point on the first reg val of the set = skip the misc member */	
	cur_reg_val =  (__le32*)set;
	++cur_reg_val;

	for (reg = 0; reg < atb_mc_reg_tbl->addrs_n; ++reg) {
		if (regs_val_location[reg] == REG_IDX_MISC_REG_VAL_FROM_TBL) {
			atb_mc_reg_tbl->sets[set_idx].vals[reg]
					= get_unaligned_le32(cur_reg_val);
			++cur_reg_val;
		} else if (regs_val_location[reg]
					== REG_IDX_MISC_REG_VAL_FROM_PREV_VAL) {
			atb_mc_reg_tbl->sets[set_idx].vals[reg]
				= atb_mc_reg_tbl->sets[set_idx].vals[reg - 1];
		}
	}
}

static long sets_cpy(struct atombios *atb, struct reg_tbl_partial *reg_tbl,
			struct atb_mc_reg_tbl *atb_mc_reg_tbl, u8 module_idx,
							u8 *regs_val_location)
{
	u16 reg_idxs_sz;
	struct reg_set_partial *set;
	u16 set_sz;
	u8 set_idx;
	u32 set_misc;

	reg_idxs_sz = get_unaligned_le16(&reg_tbl->idxs_sz);
	set = (struct reg_set_partial*)((u8*)reg_tbl + sizeof(*reg_tbl)
								+ reg_idxs_sz);
	set_sz = get_unaligned_le16(&reg_tbl->set_sz);

	set_idx = 0;
	while (1) {
		u8 cur_module_idx;

		set_misc = get_unaligned_le32(&set->misc);

		if ((set_misc == REG_SET_MISC_TERMINATOR) || (set_idx
							== REG_SETS_N_MAX))
			break;

		cur_module_idx = get(REG_SET_MISC_MODULE_IDX, set_misc);
		if (module_idx == cur_module_idx) {
			u32 mem_clk_max;

			mem_clk_max = get(REG_SET_MISC_CLK_MAX, set_misc);
			atb_mc_reg_tbl->sets[set_idx].mem_clk_max
								= mem_clk_max;

			regs_vals_cpy(atb_mc_reg_tbl, regs_val_location,
								set_idx, set);

			++set_idx;
		}
		set = (struct reg_set_partial*)((u8*)set + set_sz);
	}
	if (set_misc != REG_SET_MISC_TERMINATOR) {
		dev_err(atb->adev.dev, "atombios:vram_info corrupted sets of memory controller register values\n");
		return -ATB_ERR;
	}
	atb_mc_reg_tbl->sets_n = set_idx;
	return 0;
}

long atb_vram_info(struct atombios *atb, struct atb_mc_reg_tbl *atb_mc_reg_tbl)
{
	u16 of;
	struct master_data_tbl *data_tbl;
	struct vram_info *info;
	u32 s4;
	u8 module_idx;
	struct reg_tbl_partial *reg_tbl;
	long r;
	u8 regs_val_location[REGS_N_MAX];

	of = get_unaligned_le16(&atb->hdr->master_data_tbl_of);
	data_tbl = atb->adev.rom + of;

	of = get_unaligned_le16(&data_tbl->list.vram_info);
	info = atb->adev.rom + of;

	if (info->hdr.tbl_fmt_rev != 2 && info->hdr.tbl_content_rev != 1) {
		dev_err(atb->adev.dev, "atombios:vram_info (0x%04x) revision %u.%u not supported\n",
			of, info->hdr.tbl_fmt_rev, info->hdr.tbl_content_rev);
		return -ATB_ERR;
	}

	/* get the module index from the bios scratch register */
	s4 = atb->adev.rr32(atb->adev.dev, S4);
	module_idx = (u8)((s4 >> 16) & 0xff);

	of = get_unaligned_le16(&info->mem_clk_patch_tbl_of);
	reg_tbl = (struct reg_tbl_partial*)((u8*)info + of);

	r = regs_addr_cpy(atb, reg_tbl, atb_mc_reg_tbl, &regs_val_location[0]);
	if (r == -ATB_ERR)
		return -ATB_ERR;

	r = sets_cpy(atb, reg_tbl, atb_mc_reg_tbl, module_idx,
							&regs_val_location[0]);
	if (r == -ATB_ERR)
		return -ATB_ERR;
	return 0;
}
EXPORT_SYMBOL_GPL(atb_vram_info);
